{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Single-Qubit Calibration\n",
    "\n",
    "\n",
    "*Copyright (c) 2021 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Outline\n",
    "\n",
    "This tutorial introduces how to calibrate the qubit transition frequency, $\\pi$ pulse, relaxation times $T_1$ and $T_2$ of a single qubit using Quanlse. The outline of this tutorial is as follows:\n",
    "- Introduction\n",
    "- Preparation\n",
    "- Single-qubit simulator initialization\n",
    "- Qubit frequency calibration\n",
    "- $\\pi$ pulse calibration by Rabi oscillation\n",
    "- Calibrate $T_1$ by longitudinal relaxation\n",
    "- Calibrate $T_2$ by Ramsey oscillation\n",
    "- Summary"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Introduction\n",
    "\n",
    "Due to the imperfection in the manufacturing process and the needs of practical applications, each superconducting qubit has unique properties such as the qubit frequency, the qubit relaxation times. Therefore, each qubit requires a thorough calibration. By calibration, we perform a series of operations on the qubit and measure its response. Information regarding the qubit's properties can then be obtained, including the qubit frequency, the corresponding $\\pi$ pulse parameters, the qubit relaxation times $T_1$ and $T_2$. The qubit frequency is the driving frequency of pulse signal to realize single qubit gate. The relaxation times are the duration for which a qubit keeps its information. The longer the relaxation time, the better the quality of the qubit and the longer the time available for operation."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Preparation\n",
    "\n",
    "We first import the necessary packages from Quanlse and other commonly used Python libraries."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from Quanlse.Superconduct.Simulator import pulseSim1Q\n",
    "from Quanlse.Superconduct.Calibration.SingleQubit import qubitSpec, ampRabi, fitRabi, longRelax, ramsey, fitRamsey\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "plt.rcParams[\"figure.figsize\"] = (7, 5)\n",
    "\n",
    "from numpy import array, pi, exp\n",
    "from scipy.signal import find_peaks"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Single-qubit simulator initialization\n",
    "\n",
    "Then we proceed to define the system. Since we are dealing with a one-qubit system, we can directly import the built-in simulator `pulseSim1Q()` in Quanlse. The function `pulseSim1Q()` takes two parameters: `dt` is the AWG sampling time, and `frameMode` indicates which frame we would like to perform our experiment in. With the simulator object created, we can specify a few parameters such as the qubit frequency and qubit relaxation times $T_1$ and $T_2$. While here we do have prior knowledge of the qubit's properties, we will treat the system as a black box in the following sessions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# AWG sampling time\n",
    "dt = 0.01\n",
    "\n",
    "# Instantiate the simulator object\n",
    "model = pulseSim1Q(dt=dt, frameMode='lab')\n",
    "\n",
    "# Define system parameters\n",
    "model.qubitFreq = {0: 5.212 * (2 * pi)}\n",
    "model.T1 = {0 : 2000}\n",
    "model.T2 = {0 : 600}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qubit frequency calibration\n",
    "\n",
    "Before we calibrate any other qubit parameters, we need to identify the frequency of the qubit we are working with. Once we obtain the qubit frequency, we can correctly set the frequency of the local oscillator so that the applied pulse is in resonance with the qubit.\n",
    "\n",
    "To find the qubit frequency, we apply a pulse of reasonable amplitude to the qubit while varying the frequency of the local oscillator. When the frequency is in resonance with the qubit, the pulse can excite the qubit from its ground state. In actual experiments, the chip is handed to the experimenters with an approximation of the qubit frequency. Thus, we will first scan a range of frequencies centered at the given frequency, and pin down the accurate qubit frequency.\n",
    "\n",
    "We first scan a broader range from 4.6 GHz to 5.8 GHz. Here, we can use the function `qubitSpec()` from the calibration module. This function takes a pulseModel `model` , a range of frequency `freqRange` , sample size `sample` , pulse amplitude `amp` and duration `t`. The function then returns the scanned frequency list and the corresponding excited state populations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define frequency range\n",
    "freqRange = [4.1 * (2 * pi), 5.9 * (2 * pi)]\n",
    "\n",
    "# Scan qubit frequency spectrum\n",
    "freqList, popList = qubitSpec(pulseModel=model, freqRange=freqRange, sample=50, amp=0.9, t=20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we plot the excited state populations with respect to the LO frequencies."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Convert unit\n",
    "freq = [x / (2 * pi) for x in freqList]\n",
    "\n",
    "# Plot population graph\n",
    "plt.plot(freq, popList)\n",
    "plt.title(\"Frequency spectrum\", size=17)\n",
    "plt.xlabel(\"LO frequency (GHz)\", size=15)\n",
    "plt.ylabel(r\"$|1\\rangle$ population)\", size=15)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "From the graph we can see that the qubit frequency is somewhere between 5.1GHz and 5.3GHz. To acquire a more accurate value, we run another scan in a narrower range."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define new frequency range\n",
    "nFreqRange = [5.1 * (2 * pi), 5.3 * (2 * pi)]\n",
    "\n",
    "# Scan qubit frequency spectrum\n",
    "nFreqList, nPopList = qubitSpec(model, nFreqRange, 30, 0.9, 20)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "# Convert unit\n",
    "nFreq = [x / (2 * pi) for x in nFreqList]\n",
    "\n",
    "# Plot population graph\n",
    "plt.plot(nFreq, nPopList)\n",
    "plt.title(\"Frequency spectrum\", size=17)\n",
    "plt.xlabel(\"LO frequency (GHz)\", size=15)\n",
    "plt.ylabel(r\"$|1\\rangle$ population)\", size=15)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then, we use the function `find_peak()` from `scipy` to locate the frequency corresponding to the maximum value on the peak."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Find peak\n",
    "peak = find_peaks(nPopList, height=0.3)[0][0]\n",
    "qubitFreq = nFreq[peak]\n",
    "\n",
    "# Plot peak\n",
    "plt.plot(nFreq, nPopList)\n",
    "plt.title(f'Qubit frequency: {round(qubitFreq, 6)} GHz', size=17)\n",
    "plt.plot(nFreq[peak], nPopList[peak], 'x', mfc=None, mec='red', mew=2, ms=8)\n",
    "plt.xlabel('Frequency (GHz)', size=15)\n",
    "plt.ylabel(r'$|1\\rangle$ population', size=15)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We find the qubit frequency to be 5.217241 GHz shown in the graph above."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## $\\pi$ pulse calibration by Rabi oscillation\n",
    "\n",
    "With the qubit frequency identified, we can now calibrate the $\\pi$ and $\\pi/2$ pulse of the system. To do so, we perform a Rabi oscillation. There are generally two ways of performing Rabi oscillation: we can scan either the pulse amplitudes or durations while having the other fixed. With an appropriate range chosen, the population of the excited (or ground) state will oscillate as a sinuous function. To run a rabi oscillation in Quanlse, we import the function `ampRabi()` from the `calibration` module. The function `ampRabi()` takes 4 arguments: pulseModel `model` , amplitudes' range `ampRange` , pulse duration `tg` and the sample size `sample`. The function then returns a list of amplitudes scanned and a list of corresponding excited state populations.\n",
    "\n",
    "Note that the `calibration` module also includes function `tRabi()` which scans the $\\pi$ pulse duration. This function is quite similar to `ampRabi()`, thus will not be demonstrated here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define amplitude range\n",
    "ampRange = [0, 6]\n",
    "\n",
    "# Scan different amplitudes for Rabi oscillation\n",
    "ampList, popList = ampRabi(pulseModel=model, pulseFreq=qubitFreq * 2 * pi,\n",
    "                           ampRange=ampRange, tg=20, sample=50)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can plot the excited state populations with respect to the pulse amplitudes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Plot Rabi Oscillation with different amplitudes\n",
    "plt.plot(ampList, popList, '.')\n",
    "plt.title(\"Rabi Oscillation\", size=17)\n",
    "plt.xlabel('Amplitude', size=15)\n",
    "plt.ylabel(r'$|1\\rangle$ population', size=15)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then, using the `fitRabi()` function from the `calibration` module, we can fit the curve with a cosine function and obtain the amplitudes for the $\\pi$ and $\\pi/2$ pulse. We input the list of amplitudes `ampList` as X axis and the list of populations `popList` as Y axis. The fitting function takes the form: $y = a \\cdot cos(b \\cdot x+c)+d$. The function `fitRabi()` returns two values, first of which is the $\\pi/2$ pulse amplitude and the second being that of a $\\pi$ pulse."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Fit Rabi\n",
    "halfPiAmp, piAmp = fitRabi(popList=popList, xList=ampList)\n",
    "print(\"Pi/2 pulse amplitude: \", halfPiAmp)\n",
    "print(\"Pi pulse amplitude: \", piAmp)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Calibrate $T_1$ by longitudinal relaxation\n",
    "\n",
    "Now we have obtained the $\\pi$ and $\\pi/2$ pulse configurations. We can further calibrate the qubit relaxation times $T_1$ and $T_2$. To obtain $T_1$, we first apply a $\\pi$-pulse on the qubit and find the time when the population of the excited state decays to $1/e$ \\[1\\].\n",
    "\n",
    "To excite the qubit to the excited state and to observe the longitudinal relaxation, we can use the function `longRelax()` from the `Quanlse.Calibration.SingleQubit` module. The function arguments include: a pulseModel object `model`, AWG sampling time `dt`, qubit frequency, $\\pi$ Pulse amplitude `piAmp` and duration(`20`), maximum idling time `maxIdle` and an initial value for the fitting function `initFit`. The function would then run the simulation and fit the population curve with function $y = e^{-x/T_1}$ ."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Longitudinal relaxation on a qubit\n",
    "T1, tList, experimental, fitted = longRelax(pulseModel=model, dt=dt, pulseFreq=qubitFreq * 2 * pi,\n",
    "                                            piAmp=piAmp, piLen=20, maxIdle=4000, initFit=[1500])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function returns the estimated $T_1$, a list of idling times, a list of experimentally obtained populations and a list of populations estimated by the fitting function. We can plot the resulting populations with respect to the idling time:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Print estimated T1\n",
    "print(\"Estimated T1: \", T1, \"ns\")\n",
    "\n",
    "# Plot fit result\n",
    "plt.plot(tList, experimental, \"+\", label=\"Experiment\")\n",
    "plt.plot(tList, fitted, \"r\", label=\"Fitted\", linewidth=2.)\n",
    "plt.legend()\n",
    "plt.xlabel(\"Idling time\", size=15)\n",
    "plt.ylabel(r'$|1\\rangle$ population', size=15)\n",
    "plt.title(\"Longitudinal Relaxation\", size=17)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Calibrate $T_2$ by Ramsey oscillation\n",
    "\n",
    "To find $T_2$ which characterizes the qubit's depolarization, we perform a Ramsey oscillation on the qubit. To do so, we first apply a $\\pi/2$ pulse at drive frequency slightly detuned from the qubit frequency. After an idling time of $t_{\\rm idle}$ , we apply another $\\pi/2$ pulse and measure the excited state population of the qubit \\[2\\]. Now the measurement outcome depends on the phase of the quantum state accumulated in the idling time $t_{\\rm idle}$.\n",
    "\n",
    "To perform a Ramsey experiment on the qubit, we use the function `ramsey()` in the `Quanlse.Calibration.SingleQubit` module. This function takes a pulseModel object `model` , qubitFreq `qubitFreq`, $\\pi/2$ pulse duration `tg`, $\\pi/2$ pulse amplitude `halfPiAmp`, sample number `sample`, maximum idling time `maxTime` and the amplitude of the detuning pulse `detuning` (This simulation may take some time and you can reduce it by setting lesser sample points with worse simulation result)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Scan different idle time for Ramsey oscillation\n",
    "tList, popList = ramsey(pulseModel=model, pulseFreq=5.21 * 2 * pi, tg=20, x90=1.013,\n",
    "                        sample=50, maxTime=600, detuning=0.07)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function then returns a list of idling times and a corresponding list of populations. We can fit the results using the function `fitRamsey()` which takes the system's $T_1$ `t1`, a list of populations `popList`, a list of idling times `tList` and an amplitude of the detuning wave `detuning`. This function then fits the curve with function $y = \\frac{1}{2} cos(a\\cdot x)e^{-b\\cdot x} + 0.5$. With the coefficients found, we can find $T_2$ using the equation: $T_2 = \\frac{1}{(b-\\frac{1}{2a})}$ ."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Fit Ramsey\n",
    "T2, fitted = fitRamsey(2000, popList=popList, tList=tList, detuning=0.07)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`fitRamsey()` returns the estimated $T_2$ value and a list of the populations on the fitted curve. We can plot the excited state population with respect to the idling time as shown below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Print estimated T2\n",
    "print(\"Estimated T2: \", T2, \" ns\")\n",
    "\n",
    "# Plot fit result\n",
    "plt.plot(tList, popList, '.')\n",
    "plt.plot(tList, fitted)\n",
    "plt.plot(tList, list(exp(- (1 / 600 + 1 / (2 * 2000)) * array(tList)) * 0.5 + 0.5))\n",
    "plt.xlabel(\"Idling time (ns)\", size=15)\n",
    "plt.ylabel(r\"$|1\\rangle$ population\", size=15)\n",
    "plt.title(\"Ramsey Experiment\", size=17)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "This tutorial introduces how to use Quanlse for calibrations of single qubit frequency、$\\pi$ pulse、relaxation time $T_1$ and dephasing time $T_2$. The users could follow this link [tutorial-single-qubit-calibration.ipynb](https://github.com/baidu/Quanlse/blob/main/Tutorial/EN/tutorial-single-qubit-calibration.ipynb) to the GitHub page of this Jupyter Notebook document and run this program for themselves. The users are encouraged to try parameter values different from this tutorial to obtain the optimal result. At the same time, after reading this tutorial, readers should have a general understanding of the single qubit calibration process. \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Reference\n",
    "\n",
    "\\[1\\] [Krantz, Philip, et al. \"A quantum engineer's guide to superconducting qubits.\" *Applied Physics Reviews* 6.2 (2019): 021318.](https://doi.org/10.1063/1.5089550)\n",
    "\n",
    "\\[2\\] [Ramsey, Norman F. \"A molecular beam resonance method with separated oscillating fields.\" *Physical Review* 78.6 (1950): 695.](https://doi.org/10.1103/PhysRev.78.695)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
